package observer;

import util.ArrayUtil;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * These strings can be observed by {@link StringObserver}s, i.e. the observers will be notified of any changes.
 *
 * @author Neil Dickson
 */
public final class ObservedString implements Comparable, Copiable<ObservedString>, Serializable {
	private char[]	value;
	private int		length;

	private ArrayList<StringObserver> observers;

	public static AtomicInteger INSTANCE_COUNT = new AtomicInteger(0);

	public ObservedString() {
		this(16);
	}
	public ObservedString(String s) {
		if (s==null) {
			s = "";
		}
		value = s.toCharArray();
		length = s.length();
		observers = null;
		INSTANCE_COUNT.incrementAndGet();
	}
	public ObservedString(char[] s) {
		this(s,s.length);
	}
	public ObservedString(char[] s,int length) {
		if (s==null) {
			s = new char[0];
		}
		length = Math.max(0,Math.min(length,s.length));
		value = new char[length];
		System.arraycopy(s,0,value,0,length);
		this.length = length;
		observers = null;
		INSTANCE_COUNT.incrementAndGet();
	}
	public ObservedString(int capacity) {
		value = new char[capacity];
		length = 0;
		observers = null;
		INSTANCE_COUNT.incrementAndGet();
	}
	public synchronized void addObserver(StringObserver observer) {
		if (observer==null) {
			throw new NullPointerException();
		}
		if (observers==null) {
			observers = new ArrayList<StringObserver>();
		}
		if (!observers.contains(observer)) {
			observers.add(observer);
		}
	}
	public synchronized void removeObserver(StringObserver observer) {
		if (observers!=null && observers.contains(observer)) {
			observers.remove(observer);
		}
	}

	public synchronized void insert(StringObserver culprit,int index,char character) {
		if (index<0 || index>length) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
		String oldValue = toString();
		ensureCapacity(length+1);
		for (;index<length;++index) {
			char temp = value[index];
			value[index] = character;
			character = temp;
		}
		value[index] = character;
		++length;
		updateAllBut(culprit,oldValue);
	}
	public synchronized void insert(StringObserver culprit,int index,String s) {
		insert(culprit,index,s.toCharArray());
	}
	public synchronized void insert(StringObserver culprit,int index,char[] chars) {
		if (index<0 || index>length) {
			throw new ArrayIndexOutOfBoundsException(index);
		}
		String oldValue = toString();
		ensureCapacity(length+chars.length);
		for (int i=length-1;i>=index;++i) {
			value[i+chars.length] = value[i];
		}
		System.arraycopy(chars,0,value,index,chars.length);
		length+=chars.length;
		updateAllBut(culprit,oldValue);
	}
	public synchronized void append(StringObserver culprit,char character) {
		insert(culprit,length,character);
	}
	public synchronized void append(StringObserver culprit,String s) {
		insert(culprit,length,s);
	}
	public synchronized void append(StringObserver culprit,char[] chars) {
		insert(culprit,length,chars);
	}
	public synchronized void setValue(StringObserver culprit,String s) {
		String oldValue = toString();
		value = s.toCharArray();
		length = s.length();
		updateAllBut(culprit,oldValue);
	}
	public synchronized void setValue(String s) {
		setValue(null,s);
	}
	public synchronized void setValue(StringObserver culprit,StringBuffer s) {
		String oldValue = toString();
		value = new char[s.length()];
		s.getChars(0,s.length(),value,0);
		length = s.length();
		updateAllBut(culprit,oldValue);
	}
	public synchronized void setValue(StringBuffer s) {
		setValue(null,s);
	}

	public synchronized boolean startsWith(String prefix) {
		return startsWith(prefix,0);
	}
	public synchronized boolean endsWith(String prefix) {
		return startsWith(prefix,length-prefix.length());
	}
	public synchronized boolean startsWith(String prefix,int toffset) {
		int po = 0;
		int pc = prefix.length();
		if ((toffset < 0) || (toffset > length - pc)) {
			return false;
		}
		while (--pc >= 0) {
			if (value[toffset++] != prefix.charAt(po++)) {
				return false;
			}
		}
		return true;
	}

	public synchronized int length() {
		return length;
	}
	public synchronized String toString() {
		return new String(value,0,length);
	}
	public synchronized char[] toCharArray() {
		return ArrayUtil.subarray(value,0,length);
	}

	private void updateAllBut(StringObserver culprit,String oldValue) {
		if (observers!=null) {
			for (StringObserver observer : observers) {
				if (observer!=culprit) {
					observer.valueChanged(this,oldValue);
				}
			}
		}
	}
	private void updateAll(String oldValue) {
		if (observers!=null) {
			for (StringObserver observer : observers) {
				observer.valueChanged(this,oldValue);
			}
		}
	}

	private void ensureCapacity(int minLength) {
		if (value.length<minLength) {
			int newLength = Math.max(2*value.length,minLength);
			char[] newValue = new char[newLength];
			System.arraycopy(value,0,newValue,0,length);
			value = newValue;
		}
	}
	private void checkReduceCapacity() {
		if (value.length<(length>>>1)) {
			int newLength = Math.max((value.length*3)>>>2,length);
			char[] newValue = new char[newLength];
			System.arraycopy(value,0,newValue,0,length);
			value = newValue;
		}
	}

	public int compareTo(Object o) {
		if (!(o instanceof ObservedString)) {
			throw new ClassCastException();
		}
		ObservedString s = (ObservedString) o;
		int minLength = Math.min(length,s.length);
		for (int i=0;i<minLength;++i) {
			if (value[i]<s.value[i]) {
				return -1;
			}
			if (value[i]>s.value[i]) {
				return 1;
			}
		}
		if (length<s.length) {
			return -1;
		}
		if (length>s.length) {
			return 1;
		}
		return 0;
	}
	public boolean equals(Object o) {
		if (!(o instanceof ObservedString) && !(o instanceof String) && !(o instanceof StringBuffer)) {
			return false;
		}
		char[] thatValue;
		if (o instanceof ObservedString) {
			thatValue = ((ObservedString)o).value;
		}
		else if (o instanceof String) {
			thatValue = ((String)o).toCharArray();
		}
		else { // o instanceof StringBuffer
			thatValue = o.toString().toCharArray();
		}
		return Arrays.equals(value,thatValue);
	}

	public ObservedString copy() {
		return new ObservedString(value,length);
	}

	public static ObservedString[] convertArray(String[] strings) {
		if (strings==null) {
			return null;
		}
		ObservedString[] newArray = new ObservedString[strings.length];
		for (int i=0;i<strings.length;++i) {
			newArray[i] = new ObservedString(strings[i]);
		}
		return newArray;
	}

	public static ObservedString[] toObservedArray(String[] modifiers) {
		if (modifiers==null) {
			return null;
		}
		ObservedString[] observed = new ObservedString[modifiers.length];
		for (int i=0;i<modifiers.length;++i) {
			observed[i] = new ObservedString(modifiers[i]);
		}
		return observed;
	}

	/**
	 * @return the list of observers, or null if none have been added
	 */
	public ArrayList<StringObserver> getObservers() {
		return observers;
	}
}
